/*
 * Created on 6. sep.. 2006
 * Created by Tomas
 */
package placing;

import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import people.DuplicatePersonException;
import people.Group;
import people.GroupNotFoundException;
import people.MembershipManager;
import people.Person;
import people.PersonNotFoundException;
import people.Reserver;
import util.Debug;

/**
 * 
 * <p>The Mapper does the actual assignation of Places to Student. It has a 
 * RoomManager which handles the Rooms and Places, and a MembershipManager 
 * which handles the Persons and Groups. The Mapper maps the Students to Places,
 * and stores this mapping.</p>
 * 
 * @author Tomas Ekeli
 *
 */
public class Mapper implements Serializable{
	private static final long serialVersionUID = 1L;
	private RoomManager mgrRooms;
	private MembershipManager mgrMembers;
	
	private LinkedHashMap<Person, Place> mapDistribution;
	private boolean bCleanMap = false;
	
	
	//For the recursive check of preferences
	private final int MAX_RECURSE = 20;
	private int recursiveDepth = 0;
	

	/**
	 * Constructs a new Mapper with no People, Rooms or mapping.
	 *
	 */
	public Mapper() {
		mgrRooms = new RoomManager();
		mgrMembers = new MembershipManager();
	}
	
	/**
	 * Returns true iff no Rooms exist
	 * 
	 * @return true iff no Rooms exist
	 */
	public boolean noRooms() {
		return mgrRooms.isEmpty();
	}
	
	/**
	 * Returns true iff no People exist
	 * 
	 * @return true iff no People exist
	 */
	public boolean noPeople() {
		return mgrMembers.getPersons().isEmpty();
	}


	/**
	 * Adds a Room to the Map. The Room will have the next number after the 
	 * currently highest room-number, and it will have no Places.
	 *
	 * @return The empty Room that was added
	 * @throws RoomException iff the Room could not be added
	 */
	public Room addRoom() throws RoomException {
		return mgrRooms.addRoom();
		
	}


	/**
	 * Adds a Room to the Map with the specified number of Places. The Room
	 * will have the next number after the currently highest room-number.
	 * 
	 * @param places The number of Places in the Room to be added
	 * @throws RoomException iff the Room could not be added
	 */
	public Room addRoom(int places) throws RoomException {
		return mgrRooms.addRoom(places);
	}


	/**
	 * Adds the specified Room to the Map. The Room wil now be under management,
	 * and the Places in it will be considered for distribution.
	 * 
	 * @param r The Room to add to the Map
	 * @throws RoomException iff The Room could not be added. Typically the room
	 * was already present, or an other room with the same uniquely identifying
	 * number was present.
	 */
	public void addRoom(Room r) throws RoomException {
		mgrRooms.addRoom(r);

		
	}
	
	/**
	 * Removes the Room with the given id from the Map.
	 * 
	 * @param id The id of the Room to remove
	 * @throws RoomException iff no such room is on the Map
	 */
	public void removeRoom(int id) throws RoomException {
		mgrRooms.removeRoom(id);
		
	}

	/**
	 * Removes the Room given from the Map. This method checks for the uniquely
	 * identifying number of r, and removes any Room on the Map with that id, 
	 * thus any existing room with id == 40 will be removed if r has an id == 40.
	 * 
	 * @param r The Room to remove
	 * @throws RoomException iff no such Room is on the Map
	 */
	public void removeRoom(Room r) throws RoomException {
		removeRoom(r.getID());
		
	}
	
	
	/**
	 * Checks whether r is a Room the Mapper knows about. This check test for
	 * identifying numbers, so if there is a Room with id == 13, a call to this
	 * method for any Room with id == 13 will return true; There can only ever
	 * be one such Room in a Map.
	 * 
	 * @param r The Room to check if exists
	 * @return true iff r is known to the Mapper
	 */
	public boolean isRoom(Room r) {
		return isRoom(r.getID());
	}

	/**
	 * Checks whether there is a Room the Mapper knows about with id as its
	 * identifying number.
	 * 
	 * @param id The id of the Room to check for
	 * @return true iff such a Room is known to the Mapper
	 */
	public boolean isRoom(int id) {
		try {
			mgrRooms.getRoom(id);
			return true;
		} catch (RoomException e) {
			return false;
		}
	}

	/**
	 * Checks how many Rooms the Map has, and returns that number
	 * 
	 * @return The number of Rooms on the Map
	 */
	public int numberOfRooms() {
		return mgrRooms.getRooms().size();
	}
	
	/**
	 * Checks how many Places are in the Rooms on the Map and returns that number.
	 * 
	 * @return The number of Places on the Map
	 */
	public int numberOfPlaces() {
		int places = 0;
		List<Room> liRooms = mgrRooms.getRooms();
		for (Room r : liRooms) {
			places += r.getPlaces().size();
		}
		return places;
	}
	
	/**
	 * Cheks how many free Places are in the Rooms on the Map and returns that
	 * number. A Place is free if it is not currently occupied.
	 * 
	 * @return The number of free Places on the Map.
	 */
	public int numberOfFreePlaces() {
		int free = 0;
		List<Room> liRooms = mgrRooms.getRooms();
		for (Room r : liRooms) {
			free += r.getNumFreePlaces();
		}
		
		return free;
	}


	/**
	 * Adds places number of Places to the Room managed with the id room.
	 * 
	 * @param places The number of Places to add to the Room
	 * @param room The identifying number of the Room where the Places should
	 * be added
	 * @throws RoomException iff there is no Room with id==room
	 */
	public void addPlaces(int places, int room) throws RoomException {
		Room actualRoom = mgrRooms.getRoom(room);
		
		for (int i = 0; i < places; i++) {
			actualRoom.addPlace();
		}
	}


	/**
	 * Adds the number of Places to r.
	 * 
	 * @param places The number of Places to add
	 * @param room The Room to add them to
	 * @throws RoomException iff r is not under management.
	 */
	public void addPlaces(int places, Room room) throws RoomException {
		addPlaces(places, room.getID());
	}
	
	/**
	 * Adds a specific Place to a specific Room.
	 * @param pl The Place to add to the Room
	 * @param room The identifying number of the Room to add the Place to
	 * @throws RoomException iff the Room is not on the Map
	 * @throws PlaceNotFoundException The Place is somehow invalid (ie. it could
	 * have a negative place-id or something of the kind)
	 */
	public void addThisPlace(Place pl, int room) throws RoomException, PlaceNotFoundException {
		Room actualRoom = mgrRooms.getRoom(room);
		actualRoom.addPlace(pl);
	}

	/**
	 * Adds a specific Place to a specific Room.
	 * @param pl The Place to add to the Room
	 * @param room The Room to add the Place to
	 * @throws RoomException iff the Room is not on the Map
	 * @throws PlaceNotFoundException The Place is somehow invalid (ie. it could
	 * have a negative place-id or something of the kind)
	 */
	public void addThisPlace(Place pl, Room room) throws RoomException, PlaceNotFoundException {
		addThisPlace(pl, room.getID());
		
	}


	/**
	 * Removes the number of Places from the Room with the ID given. Will only
	 * remove free Places.
	 * 
	 * @param places The number of Places to Remove
	 * @param room The Room to remove them from
	 * @throws RoomException iff the Room is not managed
	 * @throws PlaceNotFoundException iff there aren't that many Places in that
	 * Room, or there aren't that many free Places in that Room.
	 */
	public void removePlaces(int places, int room) throws RoomException, PlaceNotFoundException {
		Room actualRoom = mgrRooms.getRoom(room);
		
		if (actualRoom.getNumPlaces() < places) {
			throw new PlaceNotFoundException("Cannot remove " + places 
					+ " places from a Room with " + actualRoom.getNumPlaces()
					+ " places.");
		}
		
		/*
		 * For now the Mapper will not remove places that are occupied.
		 * It might be better if the Mapper removes the Places, and returns the
		 * now unassigned Persons?
		 */
		if (actualRoom.getNumFreePlaces() < places) {
			throw new PlaceNotFoundException("Cannot remove Places that are occupied.");
		}
		
		//remove the number of places given
		actualRoom.removeFreePlaces(places);
		
		
	}
	
	/**
	 * Removes the number of Places from the Room . Will only remove free Places.
	 * 
	 * @param places The number of Places to Remove
	 * @param room The Room to remove them from
	 * @throws RoomException iff the Room is not managed
	 * @throws PlaceNotFoundException iff there aren't that many Places in that
	 * Room, or there aren't that many free Places in that Room.
	 */
	public void removePlaces(int places, Room room) throws RoomException, PlaceNotFoundException {
		removePlaces(places, room.getID());
	}


	/**
	 * Removes all the Places from the Room given. Only free Places will be removed.
	 * @param room The room to empty
	 * @throws RoomException iff the Room is not managed
	 * @throws PlaceNotFoundException 
	 */
	public void removeAllFreePlaces(int room) throws RoomException {
		Room actualRoom = mgrRooms.getRoom(room);
		
		
		try {
			actualRoom.removeFreePlaces(actualRoom.getNumFreePlaces());
		} catch (PlaceNotFoundException e) {
			//this should not, and cannot happen
			throw new RoomException("Problem removing free places: " +  e.getMessage());
		}
	}
	
	/**
	 * Removes all Places in the Room that are not currently occupied.
	 * 
	 * @param room The Room to remove all free Places in
	 * @throws RoomException iff the Room is not on the Map
	 */
	public void removeAllFreePlaces(Room room) throws RoomException {
		removeAllFreePlaces(room.getID());
	}

	/**
	 * Adds a Person to the Map. This does not assign the Person. A Person can
	 * only be added once, any further attempts to add that Person will be
	 * ignored.
	 * 
	 * @param pers The Person to add
	 */
	public void addPerson(Person pers) {
		try {
			mgrMembers.addPerson(pers);
		} catch (DuplicatePersonException e) {
			// ignore the addition, then.
		}
	}
	
	/**
	 * Adds a Group to the Map. This does not assign the Persons in the Group,
	 * but if they were not under management before the addition of the Group
	 * they will be after. Re-adding a group will make sure all current members
	 * are under management, but there will only be one of each Group.
	 * 
	 * @param g The Group to add
	 */
	public void addGroup(Group g) {
		mgrMembers.addGroup(g);
	}
	
	/**
	 * Find out how many Persons are known. This will always be less than or
	 * equal to the number of assigned Persons.
	 * 
	 * @return The number of Persons known
	 */
	public int numberOfPersons() {
		return mgrMembers.getNumberOfPersons();
	}

	/**
	 * Removes the Person pers from the Map.
	 * 
	 * @param pers The person to remove
	 * @throws PersonNotFoundException iff pers was not managed.
	 */
	public void removePerson(Person pers) throws PersonNotFoundException {
		mgrMembers.removePerson(pers);
	}
	
	/**
	 * Removes the Person with the given ID from the Map.
	 * 
	 * @param personID The ID of the Person to remove
	 * @throws PersonNotFoundException iff no person with that ID was managed.
	 */
	public void removePerson(int personID) throws PersonNotFoundException {
		mgrMembers.removePerson(personID);
	}


	/**
	 * Removes the Person from the Group if it is a member. If pers is not a
	 * member of g nothing will happen.
	 * 
	 * @param pers The Person to remove from g.
	 * @param g The Group where pers is no longer a member.
	 */
	public void removePersonFromGroup(Person pers, Group g) {
		mgrMembers.removePersonFromGroup(pers, g);
	}


	/**
	 * Assigns the Person to the Group given
	 * @param pers The Person to assign
	 * @param g The Group pers should be a member of
	 */
	public void assignPersonToGroup(Person pers, Group g) {
		mgrMembers.addPersonToGroup(pers, g);
	}
	
	/**
	 * Fetches the Group with the name specified. 
	 * 
	 * @param name The name of the Group
	 * @return The Group
	 * @throws GroupNotFoundException iff that Group is unknown
	 */
	public Group getGroup(String name) throws GroupNotFoundException {
		List<Group> liGroups = mgrMembers.getGroups();
		for (Group g : liGroups) {
			if (g.getName() == name) {
				return g;
			}
		}
		
		throw new GroupNotFoundException("No group named " + name);
	}


	/**
	 * Reserves all the Places in the Room for the Reserver.
	 * 
	 * @param room The Room to reserve Places in
	 * @param res The Reserver of the Places
	 * @throws RoomException iff room is not on the Map
	 */
	public void reserveRoom(Room room, Reserver res) throws RoomException {
		mgrRooms.reserveRoom(res, room);
	}

	/**
	 * Reserves one Place in the Room given for the Reserver.
	 * 
	 * @param room The Room to reserve a Place in
	 * @param res The Reserver
	 * @throws PlaceNotFoundException iff there are no places in the Room
	 * @throws RoomException iff the Room is not on the Map
	 */
	public void reservePlaceInRoom(Room room, Reserver res) throws PlaceNotFoundException, RoomException {
		mgrRooms.reserveAnyPlaceInRoom(res, room);
	}
	
	/**
	 * Reserves the Place given for the Reserver
	 * 
	 * @param pl The Place to reserve
	 * @param res The Reserver of the Place
	 * @throws PlaceNotFoundException iff the Place is not on the Map
	 */
	public void reservePlace(Place pl, Reserver res) throws PlaceNotFoundException {
		mgrRooms.reservePlace(res, pl);
	}

	/**
	 * Checks whether the Person is valid for the Place.
	 * 
	 * @param pers The Person to validate
	 * @param freePlace The Place to check the Person on
	 * @return <code>true</code> iff <code>pers</code> can be placed at 
	 * <code>freePlace</code>.
	 */
	public boolean isValid(Person pers, Place freePlace) {
		return freePlace.isValidAndFree(pers);
	}

	/**
	 * Assigns the Person to a Place if there is one available.
	 * @param p The Person to assign a Place
	 * @throws PersonNotFoundException iff the Person is unknown
	 * @throws PlaceNotFoundException iff no free place is available to the Person
	 * @throws ReservationConflictException 
	 */
	public void grantPersonPlace(Person p) throws PersonNotFoundException, 
	PlaceNotFoundException, ReservationConflictException {
		Person grantee = mgrMembers.getPerson(p.getID());
		
		if (Debug.ON) {
			System.out.println("\nLooking for place for: " + grantee.getName());
		}
		
		//Check that grantee needs a place
		if (grantee.isAssigned()) {
			if (Debug.ON) {
				System.out.println(grantee.getName() + " is already assigned!");
			}
			Place curPlace = grantee.getCurrentPlace();
			try {
				Room curRoom = mgrRooms.getRoom(curPlace.getRoomID());
				if (curRoom.contains(curPlace) && curPlace.isValid(p)) {
					if (Debug.ON) {
						System.out.println(p.getName() + " is already assigned and ok");
					}
					return; //Don't move person, it's OK
				}
			} catch (RoomException e) {
				//Room is no longer available
			}
		}
		
		//Check preferred Place
		if (grantee.hasPreferredPlace()) {
			Place plPreferred = grantee.getPreferredPlace();
			
			if (plPreferred.isValidAndFree(grantee) && (plPreferred.getPreferredBy() == grantee)) {
				plPreferred.assign(p);
				return;
			}
		}
		
		
		//Get a random free Place
		Place newPlace = mgrRooms.getRandomFreePlace(grantee);
		if (Debug.ON) {
			System.out.println("Considering " + newPlace.getPlaceName());
		}
		
		//if that place has preferences, see if we can go elsewhere
		newPlace = checkPreferations(newPlace, grantee);
		
		if (Debug.ON) {
			System.out.println("Assigning " + p.getName() 
					+ " to " + newPlace.getPlaceName());
		}
		newPlace.assign(p);			
		
	}

	/**
	 * Grants Places to the members of the Group. The distribution of Places is
	 * based on the score of the members of the Group, highest score getting 
	 * first pick. Will only grant a Place to a Person if that person is a valid
	 * owner of the Place (ie. it is free, and if it is reserved it is reserved
	 * for that Person).
	 * 
	 * @param g The Group to grant Places to.
	 * @throws ReservationConflictException iff the Persons were assigned to
	 * Places they shouldn't have been
	 */
	public void grantGroupPlace(Group g) throws ReservationConflictException {
		List<Person> liPersons = g.getPersons();
		Collections.sort(liPersons); //assign according to score
		
		distributePersons(liPersons);
		
	}
	
	public void printout() {
		mgrRooms.printout();
	}


	/**
	 * Grants Places to the people managed. The Places are distributed according
	 * to the score of the Persons, with the highest scoring People getting
	 * first pick. Places that are reserved will only get distributed among
	 * Persons that fulfill the reservation.
	 * @throws ReservationConflictException iff People were assigned to Places
	 * they shouldn't have been
	 *
	 */
	public void distributePersonsToPlaces() throws ReservationConflictException {
		List<Person> liPersons = mgrMembers.getPersons();
		
		distributePersons(liPersons);
	}


	/**
	 * Fetches the map that contains the distribution of Persons to Places.
	 * 
	 * @return The distribution
	 */
	public Map<Person, Place> getMap() {
		if (bCleanMap) {
			return new LinkedHashMap<Person, Place>(mapDistribution);
		}
		
		//make a new map
		mapDistribution = new LinkedHashMap<Person, Place>();
		
		//iterate Persons, putting assigned ones on the map
		List<Person> liPersons = mgrMembers.getPersons();
		for (Person p : liPersons) {
			if (p.isAssigned()) {
				try {
					mapDistribution.put(p, p.getCurrentPlace());
				} catch (PlaceNotFoundException e) {
					//Person assigned, but not placed. Eh?
					if (Debug.ON) {
						System.out.println("Ignoring: " + p.getName() 
								+", assigned but has no Place. Impossible.");
					}
				}
			}
		}
		
		return mapDistribution;
	}
	
	public List<Group> getGroups() {
		return mgrMembers.getGroups();
	}
	
	public List<Person> getPersons() {
		return mgrMembers.getPersons();
	}
	
	public List<Room> getRooms() {
		return mgrRooms.getRooms();
	}


	/**
	 * Recursive checking for places that are not on someone's Preferred list.
	 * Will stop recursion after MAX_RECURSE steps.
	 * 
	 * @param checkMe The Place to check
	 * @param p The Person to place
	 * @return a good choice
	 */
	private Place checkPreferations(Place checkMe, Person p) {
		//if this has gone on long enough - return checkMe
		if (recursiveDepth++ > MAX_RECURSE) {
			recursiveDepth = 0;
			if (Debug.ON) {
				System.out.println("Looked for long enough, it'll do.");
			}
			return checkMe;
		}
		
		//if newPlace is not preferred - return it
		if (!checkMe.isPreferredBy()) {
			if (Debug.ON) {
				System.out.println("Found an unpreferred Place!");
			}
			recursiveDepth = 0;
			return checkMe;
		}
		
		//look for another place
		if (Debug.ON) {
			System.out.println("Preferred place by others, looking for alternative... ");
		}
		try {
			//the recursive call
			return checkPreferations(mgrRooms.getRandomFreePlace(p), p);
		} catch (PlaceNotFoundException e) {
			if (Debug.ON) {
				System.out.println("No alternatives!");
			}	
			recursiveDepth = 0;
			return checkMe;
		}
	}


	private void distributePersons(List<Person> liPersons) throws ReservationConflictException {
		for(Person p : liPersons) {
			try {
				grantPersonPlace(p);
			} catch (PersonNotFoundException e) {
				//Person is unmanaged - ignore that
			} catch (PlaceNotFoundException e) {
				//No free place for the Person
				p.unAssign();
			} 
		}
	}


	
	public Place getPlace(String value) throws PlaceNotFoundException, RoomException {
		int separator = value.lastIndexOf('-');
		int pl = Integer.parseInt(value.substring(separator+1));
		int room = Integer.parseInt(value.substring(0, separator));
		
		Place place = mgrRooms.getRoom(room).getPlace(pl);
		return place;
	}


	/**
	 * Assigns the Person to the Place. If the components exist this will force
	 * the Person to that Place, displacing any other Person there and disregarding
	 * any reservations, in effect forcing the issue.
	 * @param p The Person to assign
	 * @param pl The Place to assign
	 * @throws PlaceNotFoundException iff the Place is not on the Map
	 * @throws RoomException iff the Room is not on the Map
	 * @throws PersonNotFoundException iff the Person is not on the Map.
	 */
	public void assignPersonToPlace(Person p, Place pl) 
	throws PlaceNotFoundException, RoomException, PersonNotFoundException {
		Place placeActual = mgrRooms.getRoom(pl.getRoomID()).getPlace(pl.getPlaceID());
		Person persActual = mgrMembers.getPerson(p.getID());
		
		//remove any current owner, return if it's p
		if (!placeActual.isFree()) {
			Person previousOwner = placeActual.getCurrent();
			if (previousOwner == persActual) {
				return;
			}
			previousOwner.unAssign();
			placeActual.clearHistory();
		}
		//remove the person from any current place
		persActual.unAssign();
		
		//the assignation
		persActual.assign(placeActual);
		try {
			placeActual.assign(persActual);
		} catch (ReservationConflictException e) {
			try {
				placeActual.reserve(persActual); //force the issue
				placeActual.assign(persActual);
			} catch (ReservationConflictException e1) {
				//impossible
				e1.printStackTrace();
			}
		}
	}


	public Person getPerson(int id) throws PersonNotFoundException {
		return mgrMembers.getPerson(id);
	}


	public int getNumPersons() {
		return mgrMembers.getNumberOfPersons();
	}
}
